//-----------------------------------------------------------------------------------------------
// Author: Andre Van Der Merwe <dart@pobox.com>
// Project: .net Compound File (http://dotnetpcf.sourceforge.net)
//
// Copyright (c) Andre Van Der Merwe 2000-2001
// Licence: BSD - see the licence.txt file or visit the project homepage
//-----------------------------------------------------------------------------------------------


//TODO add a modified flag & save-modified check

namespace dotnetpcf.CompoundFile
{
   using System;
   using System.Diagnostics;
   using System.Collections;
   using System.Data;
   using System.IO;

   public enum FileFlags : uint
   {
      ffNone = 0x00, //must be used in an equality check only
      ffSystem = 0x01,
      ffCFHeader = 0x02,
      ffAllocTable = 0x04,
      ffFileTable = 0x08,
      ffDirTable = 0x10,
      ffReadOnly = 0x20,
      ffUser = 0x40,
      ffNames = 0x80,
      ffDirProperty = 0x100,
      ffFileProperty = 0x100
   }

   /// <summary>
   /// </summary>
   /// <remarks>
   /// Files are can span multiple allocation table entries and are used to create large streams of data.
   /// See FSFilterStream which makes a File appear to be a single sequential stream of data.
   /// 
   /// Files also belong to directories, this is what creates the hierachial structure required by the
   /// compound file. However files are not physically related to the directories to which they belong.
   /// A file is related to its directory by the m_iDirID member in the FileTableEntry. Thus to "move"
   /// a file from one directory to another simply means changing this value.
   /// </remarks>
   internal class FileTable
   {
      internal class FileTableEntry
      {
         public const uint SIZE = 32;

         public uint m_iID = SysInfo.UNDEFINED;
         public uint m_iAllocTblIndex = SysInfo.UNDEFINED; //Index into alloc table
         public uint m_iNameID = SysInfo.UNDEFINED;        //CFNames ID
         public uint m_iDirID = SysInfo.UNDEFINED;         //File table ID
         public uint m_iPropID = SysInfo.UNDEFINED;        //File table ID
         public uint m_iCheckSum = 0;
         public uint m_iFlags = 0;
         public uint m_iLength = 0;

            //List of IDs of alloc blocks that this file spans - in order. Used for quick access to alloc blocks
         private ArrayList m_AllocBlockList = null;
         private FileTable m_FileTable = null;

         public FileTableEntry(  FileTable tbl  )
         {
            m_FileTable = tbl;  
         }

         public void LoadFromStream(  Stream strm  )
         {
            Debug.Assert(  null != strm  );
            if(  null == strm  )
               throw(  new CompoundFileException(  CompoundFileResult.cfrStreamIsNull  )  );

            BinaryReader rdr = new BinaryReader(  strm  );

            m_iID = rdr.ReadUInt32();
            m_iAllocTblIndex = rdr.ReadUInt32();
            m_iNameID = rdr.ReadUInt32();
            m_iDirID = rdr.ReadUInt32();
            m_iPropID = rdr.ReadUInt32();
            m_iCheckSum = rdr.ReadUInt32();
            m_iFlags = rdr.ReadUInt32();
            m_iLength = rdr.ReadUInt32();
         }

         public void SaveToStream(  Stream strm  )
         {
            Debug.Assert(  null != strm  );
            if(  null == strm  )
               throw(  new CompoundFileException(  CompoundFileResult.cfrStreamIsNull  )  );

            BinaryWriter wrtr = new BinaryWriter(  strm  );

            wrtr.Write(  m_iID  );
            wrtr.Write(  m_iAllocTblIndex  );
            wrtr.Write(  m_iNameID  );
            wrtr.Write(  m_iDirID  );
            wrtr.Write(  m_iPropID  );
            wrtr.Write(  m_iCheckSum  );
            wrtr.Write(  m_iFlags  );
            wrtr.Write(  m_iLength  );
         }

         public string Name
         {
            get{  return m_FileTable.m_cfParent.Names.GetNameByID(  m_iNameID  );  }

            set
            {
               if(   !m_FileTable.CanSetFileName(  value,  m_iDirID,  this  )   )
                  throw(  new CompoundFileException(  CompoundFileResult.cfrInvalidFileName  )  );

                  //New name?
               if(  SysInfo.UNDEFINED == m_iNameID  )
               {
                     //Dont set an empty name
                  if(  ("" == value) || (null == value)  )
                     return;
                     
                  m_iNameID = m_FileTable.m_cfParent.Names.GetNextFreeNameID();
                  m_FileTable.m_cfParent.Names.SetName(  m_iNameID,  value  );
               }
               else
               {
                  if(  ("" != value) && (null != value)  )
                     m_FileTable.m_cfParent.Names.SetName(  m_iNameID,  value  );
                  else
                  {
                        //Delete old name
                     m_FileTable.m_cfParent.Names.RemoveName(  m_iNameID  );
                     m_iNameID = SysInfo.UNDEFINED;
                  }
               }
            }
         }

         internal ArrayList AllocBlockList
         {
            get
            {
               if(  null != m_AllocBlockList  )
                  return m_AllocBlockList;

               m_AllocBlockList = new ArrayList();
               m_FileTable.BuildAllocBlockList(  ref m_AllocBlockList,  this  );
               return m_AllocBlockList;
            }

            set
            {
               if(  null != value  )
                  throw new  Exception(  "Can only set FileTableEntry.AllocBlockList to null"  );

               m_AllocBlockList = null;
            }
         }

         internal FileTable FileTableOwner
         {
            get{ return m_FileTable; }
         }
      }

      //-----------------
      private CompoundFileSys m_cfParent;
      private BlockHeader m_Header = new BlockHeader();
      private Hashtable m_Entries = new Hashtable();

      public FileTable(  CompoundFileSys cf  )
      {
         m_cfParent = cf;
         m_Header.m_iMagic = SysInfo.MAGIC_FILETBL_HEADER;  
      }

      public void SaveToStream(  FSFilterStream strm  )
      {
         Debug.Assert(  null != strm  );
         if(  null == strm  )
            throw(  new CompoundFileException(  CompoundFileResult.cfrStreamIsNull  )  );

         long iStartPos = strm.Position;
         m_Header.m_iTotalItems = (uint)m_Entries.Count;
         m_Header.SaveToStream(  strm  );

         IDictionaryEnumerator elems = m_Entries.GetEnumerator();
   
         while(  elems.MoveNext()  )
         {
               // Save the physical position of the file table's file table entry so that it can
               //  be found on load. See the comments in CompoundFile.LoadFromExistingStream
            if(  0 != ((uint)FileFlags.ffFileTable & ((FileTableEntry)elems.Value).m_iFlags)  )
               m_cfParent.Header.m_iPhysicalPosOfFileTable_FileTableEntry = (uint)strm.PhysicalStreamPosition;

               //Save the entry
            ((FileTableEntry)elems.Value).SaveToStream(  strm  );
         }
            
      }
      
      public void LoadFromStream(  FSFilterStream strm  )
      {
         Debug.Assert(  null != strm  );
         if(  null == strm  )
            throw(  new CompoundFileException(  CompoundFileResult.cfrStreamIsNull  )  );

         long iStartPos = strm.Position;
         m_Header.LoadFromStream(  strm  );

         if(  SysInfo.MAGIC_FILETBL_HEADER != m_Header.m_iMagic  )
            throw(  new CompoundFileException(  CompoundFileResult.cfrInvalidFileTableMagicNumbers  )  );


         for(  int i = 0;  i < m_Header.m_iTotalItems;  ++i  )
         {
            FileTableEntry fent = new FileTableEntry(  this  );
            fent.LoadFromStream(  strm  );
            
            Debug.Assert(  SysInfo.UNDEFINED != fent.m_iID  );
            m_Entries.Add(  (uint)(fent.m_iID),  fent  );            
         }
         
         //TODO Check that num loaded = header.numInBlock
         Debug.Assert(  m_Entries.Count == m_Header.m_iTotalItems,  "Incorrect number of file entries loaded"  );
      }

      /// <summary>
      /// Returns the entry for the file table or null if it was not found
      /// </summary>
      public FileTableEntry FindAllocTableEntry()
      {
         return FindFirstEntryByFlags(  FileFlags.ffAllocTable  );
      }

      public FileTableEntry FindFileTableEntry()
      {
         return FindFirstEntryByFlags(  FileFlags.ffFileTable  );
      }

      public FileTableEntry FindDirTableEntry()
      {
         return FindFirstEntryByFlags(  FileFlags.ffDirTable  );
      }

      public FileTableEntry FindNamesEntry()
      {
         return FindFirstEntryByFlags(  FileFlags.ffNames  );
      }

      private FileTableEntry FindFirstEntryByFlags(  FileFlags flags  )
      {
         IDictionaryEnumerator elems = m_Entries.GetEnumerator();
   
         while(  elems.MoveNext()  )
         {
            FileTableEntry ent = (FileTableEntry)elems.Value;

            if(  0 != ((uint)ent.m_iFlags & (uint)flags)  )
               return ent;
         }
         
         return null;
      }

      public uint GetBlockCount(  FileTableEntry fent  )
      {
         return (uint)fent.AllocBlockList.Count;
      }


      /// <summary>
      /// Builds a list of alloc tbl IDs that this file spans
      /// </summary>
      /// <param name="List">List to populate</param>
      /// <param name="fent">FileTableEntry for file</param>
      internal bool BuildAllocBlockList(  ref ArrayList List,  FileTableEntry fent  )
      {
         Debug.Assert(  null != List,  "BuildAllocBlockList needs a valid list"  );
         if(  null == List  )
            return false;

         Debug.Assert(  null != fent,  "BuildAllocBlockList needs a valid file table entry"  );
         if(  null == fent  )
            return false;

         AllocTable.AllocTableEntry ent = m_cfParent.AllocTable.GetAllocTableEntryByID(  fent.m_iAllocTblIndex  );


         while(  null != ent  )
         {
            List.Add(  ent.m_iID  );

            if(  SysInfo.UNDEFINED != ent.m_iNext  )
            {
               uint iID = ent.m_iNext; //!
               ent = m_cfParent.AllocTable.GetAllocTableEntryByID(  ent.m_iNext  );
               Debug.Assert(  null != ent,  "error following alloc chain - " + iID.ToString()  );
            }
            else
               break;
         }

         return true;
      }

      /// <summary>
      /// Get the alloc block at iIndex
      /// </summary>
      /// <param name="fent">The file entry</param>
      /// <param name="iIndex">The zero based index</param>
      public uint GetNthBlock(  FileTableEntry fent,  uint iIndex  )
      {
         uint iAllocID = (uint)fent.AllocBlockList[ (int)iIndex ];
         return iAllocID;
      }

      /// <summary>
      /// Alloc new blocks if nessasary so that the file will have iNumBlocks
      /// </summary>
      /// <param name="fent"> </param>
      /// <param name="iNumBlocks"> </param>
      public void EnsureFileHasNBlocks(  FileTableEntry fent,  uint iNumBlocks  )
      {
         uint iCurrentlyHas = GetBlockCount(  fent  );   

         if(  iCurrentlyHas >= iNumBlocks  )
            return;

            // The file is not long enough more blocks need to be allocated
         //for(  uint i = iCurrentlyHas;  i < iNumBlocks;  ++i  )
         //   m_cfParent.m_AllocTable.GetNewFileBlock(  fent  );

         m_cfParent.AllocTable.GetNewFileBlocks(  fent,  (iNumBlocks - iCurrentlyHas) + 1  );  //! use mod??
         Debug.Assert(  GetBlockCount( fent ) >= iNumBlocks  );
      }

      /// <summary>
      /// Create a new file table entry and the associated allocation table entries
      /// </summary>
      /// <param name="allocFlags">Allocation table flags for the new block</param>
      /// <param name="fileFlags">File table flags for the new file</param>
      /// <param name="sFileName">Name of the file (can be null or empty)</param>
      public FileTableEntry CreateNewFile(  AllocTableFlags allocFlags, FileFlags fileFlags,  string sFileName  )
      {
            //Get a allocation entry for the file
         uint iNewBlock = m_cfParent.AllocTable.GetNewAllocTableEntry(  allocFlags  );

         uint iFirstFreeFile = 0;
         while(  m_Entries.ContainsKey(  ++iFirstFreeFile  )   )
         {}

         FileTable.FileTableEntry fent = new FileTable.FileTableEntry(  this  );

         try
         {
            fent.m_iID = iFirstFreeFile;
            fent.m_iAllocTblIndex = iNewBlock;
            fent.m_iFlags = (uint)fileFlags;
            fent.m_iLength = 0;
            m_Entries.Add(  fent.m_iID,  fent  ); 
            fent.Name = sFileName;
         }
         catch(  CompoundFileException  )
         {
            fent.Name = null;
            m_Entries.Remove(  iFirstFreeFile  );
            throw;
         }
         
         return fent;
      }

      public FileTableEntry CreateNewFile(  string sFileName  )
      {
         return CreateNewFile(  AllocTableFlags.efUser,  FileFlags.ffUser,  sFileName  );
      }

      public FileTableEntry FileTableEntryFromID(  uint iID  )
      {
         return (FileTableEntry)m_Entries[ iID ];
      }

      //TODO check CanSetFileName
      public bool CanSetFileName(  string sFileName,  uint iDirID,  FileTableEntry fentCurrent  )
      {
         DirTable.DirTableEntry dirParent = m_cfParent.DirTable.GetDirTableEntryByID(  iDirID  );

            //Check that no files with the name already exist
         if(  ("" != sFileName) && (null != sFileName)  )
         {
            IDictionaryEnumerator elems = m_Entries.GetEnumerator();
            while(  elems.MoveNext()  )
            {
                  //Ignore the 'current item' item.
               if(  (null != fentCurrent) && (((FileTableEntry)elems.Value).m_iID == fentCurrent.m_iID)  )
                  continue;

               if(  null != dirParent  )
               {
                     //If there are any other files under the parent dir with the same name
                  if(  (dirParent.m_iID == ((FileTableEntry)elems.Value).m_iDirID) && (sFileName == ((FileTableEntry)elems.Value).Name)  )
                     return false;
               }
               else
               {
                     //If there are any other files off the root with the same name
                  if(  (SysInfo.UNDEFINED == ((FileTableEntry)elems.Value).m_iDirID) && (sFileName == ((FileTableEntry)elems.Value).Name)  )
                     return false;
               }
            }
         }

         return true;
      }

      public bool DeleteFileEntry(  uint iID  )
      {
         if(   (SysInfo.UNDEFINED == iID) || (!m_Entries.ContainsKey(  iID  ))   )
            return false;

         FileTableEntry fent = (FileTableEntry)m_Entries[ iID ];
         Debug.Assert(  null != fent  );

         fent.Name = null;
         DeleteFileEntry(  fent.m_iPropID  );
         m_cfParent.AllocTable.DeleteAllocEntryChain(  fent.m_iAllocTblIndex  );

         m_Entries.Remove(  iID  );
         return true;
      }

      /// <summary>
      /// Find a file table entry by its ID
      /// </summary>
      /// <param name="iID"> </param>
      public FileTable.FileTableEntry GetFileTableEntryByID(  uint iID  )
      {
         IDictionaryEnumerator elems = m_Entries.GetEnumerator();
         while(  elems.MoveNext()  )
         {
            if(  ((FileTableEntry)elems.Value).m_iID == iID  )
               return (FileTableEntry)elems.Value;
         }

         return null;
      }


      /// <summary>
      /// Adds a new file table entry
      /// </summary>
      /// <param name="iID"> </param>
      /// <param name="fent"> </param>
      public void AddNewFileTableEntry(  uint iID,  FileTable.FileTableEntry fent  )
      {
         m_Entries.Add(  iID,  fent  );
      }

         /// <summary>
         /// Number of file table entrys
         /// </summary>
      public int EntryCount
      {
         get{ return m_Entries.Count; }
      }


      public IDictionaryEnumerator GetEntryEnumerator()
      {
         return m_Entries.GetEnumerator();
      }
   }

}
